import tkinter as tk
from tkinter import ttk
from tkinter import font
import sys
import os
import csv
import threading
import subprocess
from configparser import ConfigParser

swVer = '2.5.3'
fileVer = '24.9.13.2'
resultCsv = 'result.csv'
items = []

# PowerShell command
ps_script = """Set-Location (Split-Path ($MyInvocation.MyCommand.Path));Function Get-DeviceMgrInfo {$ErrDeviceIDs = @(Get-WmiObject Win32_PnPEntity | Where {$_.ConfigManagerErrorCode -ne 0}).DeviceID;Get-WmiObject -Class Win32_PnPSignedDriver | where {($_.DeviceClass -ne "SOFTWAREDEVICE") -and ($_.HardwareID -ne $null -or (($_.HardwareID -eq $null) -and ($ErrDeviceIDs -eq $_.DeviceID)))} | Add-Member -MemberType ScriptProperty -Name Device -Value {If ($this.FriendlyName) {$this.FriendlyName} Else {$this.DeviceName} } -PassThru | Add-Member -MemberType ScriptProperty -Name IsSigned-Signer -Value {-join ($this.IsSigned,"-",$this.Signer)} -PassThru | Add-Member -MemberType ScriptProperty -Name DriverDated -Value {-join ($this.DriverDate.Substring(0,4),"-",$this.DriverDate.Substring(4,2),"-",$this.DriverDate.Substring(6,2)) } -PassThru | Add-Member -MemberType ScriptProperty -Name ErrorCode -Value {If ( $ErrDeviceIDs -eq $this.DeviceID) {$Err = (Get-WmiObject Win32_PnPEntity | where {$_.DeviceID -eq $this.DeviceID}).ConfigManagerErrorCode;If ($this.HardwareID) {$Err} Else {-join ($Err,"-",$this.DeviceID)} } } -PassThru | Select-Object -Property "DeviceClass", "Device", "DriverProviderName", "DriverVersion", "DriverDated", "IsSigned-Signer", "HardwareID", "ErrorCode"}Get-DeviceMgrInfo | Sort-Object -Descending "ErrorCode", "DeviceClass", "DriverProviderName" | Export-Csv .\\DevMgrInfo.csv -NoTypeInformation"""

def resource_path(relative_path):
    """Get the absolute path of a resource, working for PyInstaller."""
    base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
    return os.path.join(base_path, relative_path)

ico_path = resource_path('logo.ico')
img_path = resource_path('logo.gif')

def load_config(config_file):
    """Load configuration from a file."""
    config = ConfigParser(strict=False)
    config.read(config_file, encoding='utf-8')
    return config

def usage_window(parent):
    """Create a usage info window."""
    u_window = tk.Toplevel(parent)
    u_window.title("Usage Info")
    u_window.geometry("800x450")
    u_window.iconbitmap(ico_path)
    usage_info = """前置条件：- 将正确的 设备名 及 版本号 的对应信息填到 Config.ini 文件中

用法说明：
Config.ini
    [Setting]
    resultCsv 为 可选项，本程序生成的结果文件，可自行命名一个 csv 文件，默认 result.csv

    [Elements]
    其中存储正确的Device名与版本号的键值对，可通过主界面中的复选框选择设备名称是否区分大小写

执行结果：
    - DevMgrInfo.csv 本机设备信息
    - 将生成 Config.ini 中指定名称的 csv 文件 或默认的 result.csv 文件
        其中包含：
            1. 设备名（源自 Config.ini）
            2. SpecVersion（源自 Config.ini）
            3. 当前测试机中抓取的版本号（源自 DevMgrInfo.csv）
            4. 对比结果（PASS / FAIL / NOT FOUND）
    - 在程序窗口中显示结果，PASS标蓝，FAIL标红，NOT FOUND标黑
    """

    u_label = tk.Label(u_window, text=usage_info, justify="left", anchor="nw")
    u_label.pack(padx=10, pady=10, fill="both", expand=True)

def about_window(parent):
    """Create an about info window."""
    a_window = tk.Toplevel(parent)
    a_window.title("About Info")
    a_window.geometry("400x250")
    a_window.iconbitmap(ico_path)
    about_info = f"""公司名称：安徽声讯智能科技有限公司
软件名称：驱动版本检查工具
版本号：{swVer} ({fileVer})
问题反馈：yi.rao@samxvm.com
Copyright © 2024 SAMXVM All Rights Reserved.
    """

    img = tk.PhotoImage(file=img_path)
    img_label = tk.Label(a_window, image=img)
    img_label.image = img
    img_label.pack(side="top", expand=True)
    about_label = tk.Label(a_window, text=about_info, justify="left", anchor="center")
    about_label.pack(padx=10, pady=10, fill="both", expand=True)

class CaseSensitiveConfigParser(ConfigParser):
    """ConfigParser that maintains case sensitivity."""
    def optionxform(self, option):
        return option

def run_ps_script(event):
    """Start the PowerShell script in a separate thread."""
    try:
        subprocess.run(['powershell', '-Command', ps_script], capture_output=True, text=True, creationflags=subprocess.CREATE_NO_WINDOW)
        event.set()  # Signal that the script has completed
    except Exception as e:
        print(f"Error running PowerShell script: {e}")

def start_ver_chk(parent, treeview, chkbtn_var, items, event):
    """Background task to process the CSV files and update the treeview."""
    if event.wait(10):  # Wait for the event to be set or timeout
        keys = {item[0]: item[1] for item in items}  # 使用字典来提高查找效率

        try:
            with open('DevMgrInfo.csv', mode='r', encoding='utf-8') as t_f, \
                 open(resultCsv, mode='w', newline='', encoding='utf-8') as r_f:
                reader = csv.DictReader(t_f)
                writer = csv.writer(r_f)
                writer.writerow(['Device', 'SpecVersion', 'VersionInEUT', 'Result'])
                
                # 构建一个已处理的设备集合
                found_devices = set()

                for row in reader:
                    device = row['Device']
                    version = keys.get(device) or (device.lower() if chkbtn_var.get() == 0 else None)
                    if version:
                        if row['DriverVersion'] == keys[device]:
                            writer.writerow([device, keys[device], row['DriverVersion'], 'PASS'])
                        else:
                            writer.writerow([device, keys[device], row['DriverVersion'], 'FAIL'])
                        found_devices.add(device)

                # 处理未找到的设备
                for device, version in keys.items():
                    if device not in found_devices:
                        writer.writerow([device, version, '', 'NOT FOUND'])

        except Exception as e:
            print(f"Error processing CSV files: {e}")

        try:
            with open(resultCsv, mode='r', newline='', encoding='utf-8') as r_f:
                reader = csv.DictReader(r_f)
                results = [row for row in reader]
            parent.after(0, lambda: update_treeview(treeview, results))
        except Exception as e:
            print(f"Error reading result CSV: {e}")

def update_treeview(treeview, results):
    """Update the treeview with results."""
    for row in results:
        try:
            device_name = row['Device']
            for item in treeview.get_children():
                if treeview.item(item, "values")[0] == device_name:
                    tags = (row['Result'], 'fontsize') if 'grayrow' not in treeview.item(item, "tags") else (row['Result'], 'fontsize', 'grayrow')
                    treeview.item(item, tags=tags)
                    treeview.set(item, "Col3", row['VersionInEUT'])
                    treeview.set(item, "Col4", row['Result'])
                    break
        except Exception as e:
            print(f"Error updating treeview: {e}")

def btn_clicked(parent, treeview, chkbtn_var, items, event):
    """Start the background thread to process the CSV files."""
    if os.path.exists('DevMgrInfo.csv'):
        thread = threading.Thread(target=lambda: start_ver_chk(parent, treeview, chkbtn_var, items, event))
        thread.daemon = True
        thread.start()
    else:
        parent.after(200, lambda: btn_clicked(parent, treeview, chkbtn_var, items, event))

def main():
    global resultCsv, items
    root = tk.Tk()
    root.title(f"Device Version Check - {swVer} | SAMXVM")
    root.iconbitmap(ico_path)
    root.resizable(False, False)
    root.option_add("*Font", "TkDefaultFont 11")
    tree_font = font.Font(family="TkDefaultFont", size=11)

    menu_bar = tk.Menu(root)
    help_menu = tk.Menu(menu_bar, tearoff=0)
    help_menu.add_command(label="Usage", command=lambda: usage_window(root))
    help_menu.add_command(label="About", command=lambda: about_window(root))
    help_menu.add_separator()
    help_menu.add_command(label="Exit", command=root.quit)
    menu_bar.add_cascade(label="Help", menu=help_menu)
    root.config(menu=menu_bar)

    frame_data = tk.Frame(root)
    frame_data.pack(padx=10, pady=10)
    frame_ctrl = tk.Frame(root)
    frame_ctrl.pack(fill=tk.X, padx=10, pady=10)

    treeview = ttk.Treeview(frame_data, columns=("Col1", "Col2", "Col3", "Col4"), show="headings", height=20)
    treeview.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
    treeview.heading("Col1", text="Device")
    treeview.heading("Col2", text="SpecVersion")
    treeview.heading("Col3", text="VersionInEUT")
    treeview.heading("Col4", text="Result")
    treeview.column("Col1", width=300)
    treeview.column("Col2", width=150)
    treeview.column("Col3", width=150)
    treeview.column("Col4", width=150)

    treeview.tag_configure("fontsize", font=tree_font)
    treeview.tag_configure('grayrow', background='gray90')
    treeview.tag_configure('PASS', foreground='blue')
    treeview.tag_configure('FAIL', foreground='red')

    scrollbar = ttk.Scrollbar(frame_data, orient="vertical", command=treeview.yview)
    scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
    treeview.config(yscrollcommand=scrollbar.set)

    errorMsg = tk.Label(frame_ctrl, text="Config.ini file ERROR!", fg="SystemButtonFace")
    errorMsg.pack(side=tk.LEFT, padx=10, anchor='center')

    chkbtn_var = tk.IntVar()
    chkbtn = tk.Checkbutton(frame_ctrl, text="device name match case", variable=chkbtn_var, onvalue=1, offvalue=0)
    chkbtn.pack(side=tk.LEFT, padx=50, anchor='center')

    event = threading.Event()  # Create an event to signal completion
    start_button = tk.Button(frame_ctrl, text="Start Check", command=lambda: btn_clicked(root, treeview, chkbtn_var, items, event))
    start_button.pack(side=tk.LEFT, padx=50, anchor='center')

    config = CaseSensitiveConfigParser()
    if os.path.exists('Config.ini'):
        config.read('Config.ini', encoding='utf-8')
        resultCsv = config.get('Setting', 'resultCsv') if config.has_option('Setting', 'resultCsv') else 'result.csv'
        if config.has_section('Elements'):
            items = config.items('Elements')
        else:
            errorMsg.config(fg="red")
    else:
        errorMsg.config(fg="red")

    grayRow = True
    for key, value in items:
        if grayRow:
            treeview.insert("", "end", values=(key, value, "", ""), tags=('fontsize', 'grayrow'))
            grayRow = False
        else:
            treeview.insert("", "end", values=(key, value, "", ""), tags='fontsize')
            grayRow = True

    threading.Thread(target=lambda: run_ps_script(event), daemon=True).start()

    root.mainloop()

if __name__ == '__main__':
    main()
